﻿//---------------------------------------------------------------------
// <copyright file="CsdlReader.Json.cs" company="Microsoft">
//      Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
// </copyright>
//---------------------------------------------------------------------

#if NETSTANDARD2_0
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using Microsoft.OData.Edm.Csdl.CsdlSemantics;
using Microsoft.OData.Edm.Csdl.Parsing;
using Microsoft.OData.Edm.Csdl.Parsing.Ast;
using Microsoft.OData.Edm.Validation;

namespace Microsoft.OData.Edm.Csdl
{
    /// <summary>
    /// A delegate used to retrieve a reader for referenced JSON CSDL schemas.
    /// Utf8JsonReader is a ref struct, we can't distiguish which Uri want to parse, which Uri want to skip.
    /// So, use an out boolean value to indicate whether to skip parsing the input reference Uri.
    /// </summary>
    /// <param name="uri">The reference Uri.</param>
    /// <param name="skip">out a boolean value that indicates whether to skip parsing the input uri.</param>
    /// <returns>The corresponding <see cref="Utf8JsonReader"/>.</returns>
    public delegate Utf8JsonReader CsdlJsonReaderFactory(Uri uri, out bool skip);

    /// <summary>
    /// Provides CSDL-JSON parsing services for EDM models.
    /// </summary>
    public partial class CsdlReader
    {
        /// <summary>
        /// Tries parsing the given CSDL-JSON artifact for an IEdmModel.
        /// </summary>
        /// <param name="reader">The given JSON reader containing the CSDL artifact.</param>
        /// <param name="model">The model generated by parsing</param>
        /// <param name="errors">Errors reported while parsing.</param>
        /// <returns>Success of the parse operation.</returns>
        public static bool TryParse(ref Utf8JsonReader reader, out IEdmModel model, out IEnumerable<EdmError> errors)
        {
            return TryParse(ref reader, CsdlJsonReaderSettings.Default, out model, out errors);
        }

        /// <summary>
        /// Tries parsing the given CSDL-JSON artifact for an IEdmModel.
        /// </summary>
        /// <param name="reader">The given JSON reader containing the CSDL artifact.</param>
        /// <param name="settings">CSDL-JSON reader settings for current parser.</param>
        /// <param name="model">The model generated by parsing</param>
        /// <param name="errors">Errors reported while parsing.</param>
        /// <returns>Success of the parse operation.</returns>
        public static bool TryParse(ref Utf8JsonReader reader, CsdlJsonReaderSettings settings, out IEdmModel model, out IEnumerable<EdmError> errors)
        {
            EdmUtil.CheckArgumentNull(settings, nameof(settings));

            model = null;

            JsonParserContext context = new JsonParserContext(settings, source: null);

            CsdlModel mainModel = CsdlJsonParser.ParseCsdlDocument(ref reader, context);

            if (mainModel != null && !context.HasIntolerableError())
            {
                Debug.Assert(mainModel.CsdlVersion != null, "csdlVersion != null");

                List<CsdlModel> referencedAstModels = LoadReferencedCsdl(mainModel, context);

                if (!context.HasIntolerableError())
                {
                    CsdlSemanticsModel tmp = new CsdlSemanticsModel(mainModel,
                        new CsdlSemanticsDirectValueAnnotationsManager(),
                        referencedAstModels,
                        settings.IncludeDefaultVocabularies);

                    // add more referenced IEdmModels in addition to the above loaded CsdlModels.
                    if (settings.ReferencedModels != null)
                    {
                        tmp.AddToReferencedModels(settings.ReferencedModels);
                    }

                    model = tmp;
                    model.SetEdmxVersion(mainModel.CsdlVersion);
                    Version edmVersion;
                    if (CsdlConstants.EdmxToEdmVersions.TryGetValue(mainModel.CsdlVersion, out edmVersion))
                    {
                        model.SetEdmVersion(edmVersion);
                    }
                }
            }

            errors = context.Errors;
            return !context.HasIntolerableError();
        }

        /// <summary>
        /// Returns an IEdmModel for the given CSDL-JSON artifact.
        /// </summary>
        /// <param name="reader">The given JSON reader containing the CSDL artifact.</param>
        /// <returns>The model generated by parsing.</returns>
        public static IEdmModel Parse(ref Utf8JsonReader reader)
        {
            IEdmModel model;
            IEnumerable<EdmError> parseErrors;
            if (!TryParse(ref reader, out model, out parseErrors))
            {
                throw new EdmParseException(parseErrors);
            }

            return model;
        }

        /// <summary>
        /// Returns an IEdmModel for the given CSDL-JSON artifact.
        /// </summary>
        /// <param name="reader">The given JSON reader containing the CSDL artifact.</param>
        /// <param name="settings">The CSDL-JSON reader settings.</param>
        /// <returns>The model generated by parsing.</returns>
        public static IEdmModel Parse(ref Utf8JsonReader reader, CsdlJsonReaderSettings settings)
        {
            IEdmModel model;
            IEnumerable<EdmError> parseErrors;
            if (!TryParse(ref reader, settings, out model, out parseErrors))
            {
                throw new EdmParseException(parseErrors);
            }

            return model;
        }

        /// <summary>
        /// Load and parse the referenced model but ignored any further referenced model.
        /// </summary>
        /// <param name="csdlModel">The main CSDL model.</param>
        /// <param name="context">The parser context.</param>
        /// <returns>A list of CsdlModel (no semantics) of the referenced models.</returns>
        private static List<CsdlModel> LoadReferencedCsdl(CsdlModel csdlModel, JsonParserContext context)
        {
            List<CsdlModel> referencedAstModels = new List<CsdlModel>();
            if (context.Settings.JsonSchemaReaderFactory == null)
            {
                // don't try to load CSDL-JSON doc, but this.edmReferences's namespace-alias need to be used later.
                return referencedAstModels;
            }

            foreach (var edmReference in csdlModel.CurrentModelReferences)
            {
                // If nothing included, why does it exist?
                if (!edmReference.Includes.Any() && !edmReference.IncludeAnnotations.Any())
                {
                    continue;
                }

                // Skip the built-in vocabulary annotation model
                if (edmReference.Uri != null && (edmReference.Uri.EndsWith("/Org.OData.Core.V1.json", StringComparison.Ordinal) ||
                    edmReference.Uri.EndsWith("/Org.OData.Capabilities.V1.json", StringComparison.Ordinal) ||
                    edmReference.Uri.EndsWith("/Org.OData.Authorization.V1.json", StringComparison.Ordinal) ||
                    edmReference.Uri.EndsWith("/Org.OData.Validation.V1.json", StringComparison.Ordinal) ||
                    edmReference.Uri.EndsWith("/Org.OData.Community.V1.json", StringComparison.Ordinal) ||
                    edmReference.Uri.EndsWith("/OData.Community.Keys.V1.json", StringComparison.Ordinal)))
                {
                    continue;
                }

                Utf8JsonReader referencedJsonReader = context.Settings.JsonSchemaReaderFactory(new Uri(edmReference.Uri, UriKind.RelativeOrAbsolute), out bool skip);
                if (!skip)
                {
                    string source = edmReference.Uri != null ? edmReference.Uri : null;
                    CsdlJsonReaderSettings newSettings = context.Settings.Clone();

                    // set it to null to make sure stop the next level reference parsing.
                    newSettings.JsonSchemaReaderFactory = null;
                    JsonParserContext subContext = new JsonParserContext(newSettings, source);

                    CsdlModel subCsdlModel = CsdlJsonParser.ParseCsdlDocument(ref referencedJsonReader, subContext);
                    if (subCsdlModel != null && subContext.IsSucceeded())
                    {
                        // Should we compare the referenced version with the main CSDL version and report error if mismatching?
                        // So far, it's fine to ignore, because there may be scenarios where referenced schemas are at a different version.
                        subCsdlModel.AddParentModelReferences(edmReference);
                        referencedAstModels.Add(subCsdlModel);
                    }

                    context.AddRange(subContext.Errors);
                }
            }

            return referencedAstModels;
        }
    }
}
#endif